﻿namespace Jsl.FxCop
{
    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.FxCop.Sdk;

    /// <summary>
    /// NOT WORKING YET!!!
    /// </summary>
    public class UseStrongestType : BaseIntrospectionRule
    {
        private TypeNode bestReturnType;

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="FixSimplePerformanceChanges"/> class.
        /// </summary>
        public UseStrongestType()
            : base(typeof(UseStrongestType).Name, "Jsl.FxCop.Rules", typeof(UseStrongestType).Assembly)
        {
        }
        #endregion Constructors

        #region Public Methods
        public override ProblemCollection Check(Member member)
        {
            // Only process if there are debug symbols. Otherwise we can't tell if a local variable is compiler generated.
            // Compiler generated local variables are heavily reused and therefore produce a lot of false positives.
            if (!member.DeclaringType.DeclaringModule.ContainingAssembly.HasDebugSymbols)
            {
                return this.Problems;
            }

            // Only process methods.
            var method = member as Method;
            if (method == null)
            {
                return this.Problems;
            }

            // Reset so we can collect data from visiting this method.
            this.bestReturnType = null;

            var variableUsages = new MethodVariableUsage(method);

            // Skip parameters since the value coming in would be considered as setting the variable's value.
            // TODO: Still check for out parameters?
            var variablesToCheck =
                from variableUsage in variableUsages
                where (variableUsage.Value != null)
                    && !(variableUsage.Key is Parameter)
                select variableUsage;

            foreach (var entry in variablesToCheck)
            {
                // TODO: Make weeding out compiler generated variables better. Remember ALL Release mode variables
                // look like this. This is currently OK because we are skipping everything that wasn't compiled
                // in debug mode.
                if (entry.Key.Name.Name.IndexOf('$') >= 0)
                {
                    continue;
                }

                // Object is different from all other variable types since you can cram anything into it.
                // TODO: What to do with dynamic?
                if (entry.Key.Type.FullName == WellKnownTypeNames.Object)
                {
                    this.DetermineIfObjectVariableHasProblems(entry.Key, entry.Value);
                }
                else
                {
                    this.DetermineIfVariableHasProblems(entry.Key, entry.Value);
                }
            }

            //if (this.bestReturnType != null)
            //{
            //    if (!object.ReferenceEquals(this.bestReturnType, method.ReturnType))
            //    {
            //        this.Problems.Add(
            //            new Problem(
            //                this.GetNamedResolution(
            //                    "Return",
            //                    method.ReturnType.FullName,
            //                    this.bestReturnType.FullName),
            //                method.SourceContext));
            //    }
            //}

            return this.Problems;
        }

        public override void VisitReturn(ReturnNode returnInstruction)
        {
            if (returnInstruction.Expression != null)
            {
                if (this.bestReturnType == null)
                {
                    this.bestReturnType = returnInstruction.Expression.Type;
                }
                else
                {
                    this.bestReturnType = this.bestReturnType.GetCommonClass(returnInstruction.Expression.Type);
                }
            }

            base.VisitReturn(returnInstruction);
        }
        #endregion Public Methods

        #region Private Methods
        private void AddChangeClassOrStructProblem(Variable variable, VariableUsage variableUsage)
        {
            this.Problems.Add(
                new Problem(
                    this.GetNamedResolution(
                        "ChangeClassOrStruct",
                        variable.Name.Name,
                        variable.Type.Name.Name,
                        variableUsage.WeakestClassUsed.FullName),
                    variable.SourceContext));
        }

        private void AddChangeToInterfaceProblem(
            Variable variable,
            VariableUsage variableUsage,
            IEnumerable<InterfaceNode> interfaces)
        {
            if (interfaces.Count() == 1)
            {
                this.Problems.Add(
                    new Problem(
                        this.GetNamedResolution(
                            "ChangeToInterface",
                            variable.Name.Name,
                            variable.Type.Name.Name,
                            interfaces.ElementAt(0).FullName),
                        variable.SourceContext));
            }
            else
            {
                var interfaceNames =
                    from @interface in interfaces
                    select @interface.FullName;

                this.Problems.Add(
                    new Problem(
                        this.GetNamedResolution(
                            "ChangeToInterfaces",
                            variable.Name.Name,
                            variable.Type.Name.Name,
                            string.Join(" or ", interfaceNames)),
                        variable.SourceContext));
            }
        }

        private void DetermineIfObjectVariableHasProblems(Variable variable, VariableUsage variableUsage)
        {
            var hasInterfaces = variableUsage.WeakestInterfacesUsed.Any();

            if ((variableUsage.WeakestClassUsed == null)
                && hasInterfaces)
            {
                // There are only interface(s) so suggest them.
                this.AddChangeToInterfaceProblem(variable, variableUsage, variableUsage.WeakestInterfacesUsed);
            }
            else if ((variableUsage.WeakestClassUsed != null)
                && !hasInterfaces)
            {
                // There are no interfaces so use the weakest class if it is different than the variable type.
                if (!variable.Type.IsSynonymousType(variableUsage.WeakestClassUsed))
                {
                    this.AddChangeClassOrStructProblem(variable, variableUsage);
                }
            }
            else
            {
                // If the weakest class supports all the interfaces then suggest changing the variable to the interfaces.
                // Otherwise, there is no common Type to go to so don't suggest anything.
                var weakestClassSupportsAllInterfaces =
                    variableUsage.WeakestInterfacesUsed.All(
                        @interface => variableUsage.WeakestClassUsed.IsAssignableTo(@interface));
                if (weakestClassSupportsAllInterfaces)
                {
                    this.AddChangeToInterfaceProblem(variable, variableUsage, variableUsage.WeakestInterfacesUsed);
                }
            }
        }

        private void DetermineIfVariableHasProblems(Variable variable, VariableUsage variableUsage)
        {
            var weakerInterfaces =
                (from @interface in variableUsage.WeakestInterfacesUsed
                 where object.ReferenceEquals(variable.Type, @interface)
                    || @interface.IsAssignableTo(variable.Type)
                 select @interface)
                .ToArray();

            if (weakerInterfaces.Length > 0)
            {
                if (!weakerInterfaces.Contains(variable.Type))
                {
                    this.AddChangeToInterfaceProblem(variable, variableUsage, weakerInterfaces);
                }
            }
            else if (variableUsage.WeakestClassUsed != null)
            {
                if (!variable.Type.IsSynonymousType(variableUsage.WeakestClassUsed))
                {
                    AddChangeClassOrStructProblem(variable, variableUsage);
                }
            }
        }
        #endregion Private Methods
    }
}